/**
 * @ Author: luoqi
 * @ Create Time: 2024-04-19 16:56
 * @ Modified by: luoqi
 * @ Modified time: 2024-07-19 13:57
 * @ Description:
 */

#include "ymodem.h"

static void *_memset(void *dest, int c, uint32_t n)
{
    if(dest == (void *)0) {
        return (void *)0;
    } else {
        char *pdest = (char *)dest;
        while(n--) {
            *pdest++ = c;
        }
        return dest;
    }
}

static uint16_t _ymodem_crc16(const uint8_t *data, uint32_t len)
{
    uint16_t crc = 0;
    uint16_t i = 0;
    while(len--) {
        crc = crc ^ ((uint16_t)(*data++) << 8);
        for(i = 0; i < 8; i++) {
            if(crc & 0x8000) {
                crc = crc << 1 ^ 0x1021;
            } else {
                crc <<= 1;
            }
        }
    }
    return crc;
}

static int _recv_pack(YModem *ymodem)
{
    uint16_t crc = 0;
    uint8_t c = 0;
    uint16_t timeout = 0;
    static uint16_t tout = 0;
    ymodem->datasz = 0;
    if(ymodem->getc(&c, 1000) != 0) {
        if(ymodem->stat == YMODEM_STAT_STARTING){
            if(c == 0x1b){
                return -1;
            }
        }
        tout = 0;
        switch(c) {
        case YMODEM_SOH:
            ymodem->datasz = 128;
            ymodem->buf[0] = YMODEM_SOH;
            break;
        case YMODEM_STX:
            ymodem->datasz = 1024;
            ymodem->buf[0] = YMODEM_STX;
            break;
        case YMODEM_EOT:
            ymodem->putc(YMODEM_NAK);
            do {
                if(ymodem->getc(&c, 1000) != 0) {
                    if(c == YMODEM_EOT) {
                        ymodem->putc(YMODEM_ACK);
                        ymodem->putc(YMODEM_C);
                        ymodem->stat = YMODEM_STAT_FINISHING;
                        ymodem->datasz = 0;
                        return 0;
                    } else {
                        return -1;
                    }
                }
            } while(timeout++ < ymodem->timeout_s);
            return -1;
        default:
            return 0;
        }
    } else {
        tout++;
        return 0;
    }
    for(int i = 1; i < ymodem->datasz + 5; i++) {
        if(ymodem->getc(&c, 1000) != 0) {
            if(ymodem->datasz + 5 > 1030) {
                return -1;
            }
            ymodem->buf[i] = c;
        } else {
            if(timeout++ > ymodem->timeout_s) {
                _memset(ymodem->buf, 0, ymodem->datasz + 5);
                return -1;
            } else {
                continue;
            }
        }
    }
    crc = _ymodem_crc16(ymodem->buf + 3, ymodem->datasz);
    if(crc == ((ymodem->buf[ymodem->datasz + 3] << 8) | ymodem->buf[ymodem->datasz + 4])) {
        return ymodem->datasz;
    } else {
        _memset(ymodem->buf, 0, ymodem->datasz + 5);
        ymodem->datasz = 0;
        return -2;
    }
}

static int _recv_starting(YModem *ymodem)
{
    int err = 0;
    for(uint32_t i = 0; i < ymodem->timeout_s; i++) {
        ymodem->putc(YMODEM_C);
        err = _recv_pack(ymodem);
        if(err < 0) {
            ymodem->putc(YMODEM_CAN);
            ymodem->putc(YMODEM_CAN);
            ymodem->putc(YMODEM_CAN);
            ymodem->putc(YMODEM_CAN);
            ymodem->putc(YMODEM_CAN);
            break;
        } else if(err == 0) {
            continue;
        } else {
            if((ymodem->buf[1] == 0x00) && (ymodem->buf[2] == 0xff)) {
                ymodem->start_cb(ymodem->buf + 3, ymodem->datasz);
                _memset(ymodem->buf, 0, ymodem->datasz + 5);
                ymodem->putc(YMODEM_ACK);
                ymodem->putc(YMODEM_C);
                ymodem->stat = YMODEM_STAT_TRANSMITTING;
                return 0;
            } else {
                return -1;
            }
        }
    }
    return -1;
}

static int _recv_transmitting(YModem *ymodem)
{
    if(_recv_pack(ymodem) > 0) {
        if(ymodem->data_cb(ymodem->buf + 3, ymodem->datasz) < 0) {
            ymodem->putc(YMODEM_NAK);
        } else {
            ymodem->putc(YMODEM_ACK);
        }
        _memset(ymodem->buf, 0, ymodem->datasz + 5);
    } else {
        ymodem->putc(YMODEM_NAK);
        return -1;
    }
    return 0;
}

static int _recv_finishing(YModem *ymodem)
{
    if(_recv_pack(ymodem) > 0) {
        ymodem->end_cb(ymodem->buf + 3, ymodem->datasz);
        _memset(ymodem->buf, 0, ymodem->datasz + 5);
        ymodem->putc(YMODEM_ACK);
        ymodem->putc(YMODEM_ACK);
        ymodem->putc(YMODEM_ACK);
    } else {
        ymodem->putc(YMODEM_CAN);
        ymodem->putc(YMODEM_CAN);
        ymodem->putc(YMODEM_CAN);
        ymodem->putc(YMODEM_CAN);
        ymodem->putc(YMODEM_CAN);
    }
    return 0;
}

int ymodem_init(YModem *ymodem, uint8_t *buf, int(*putc)(const uint8_t), int(*getc)(uint8_t *, int),
    YModemCb start_cb, YModemCb data_cb, YModemCb end_cb)
{
    ymodem->stat = YMODEM_STAT_STARTING;
    ymodem->buf = buf;
    ymodem->putc = putc;
    ymodem->getc = getc;
    ymodem->start_cb = start_cb;
    ymodem->data_cb = data_cb;
    ymodem->end_cb = end_cb;
    return 0;
}

int ymodem_download(YModem *ymodem, uint32_t timeout_s)
{
    ymodem->timeout_s = timeout_s;
    uint32_t timeout = 0;
    while(ymodem->stat != YMODEM_STAT_FINISHED) {
        switch(ymodem->stat) {
        case YMODEM_STAT_STARTING:
            if(_recv_starting(ymodem) < 0) {
                ymodem->stat = YMODEM_STAT_STARTING;
                return -1;
            } else {
                continue;
            }
        case YMODEM_STAT_TRANSMITTING:
            if(_recv_transmitting(ymodem) < 0) {
                if(timeout++ > ymodem->timeout_s) {
                    ymodem->stat = YMODEM_STAT_STARTING;
                    ymodem->putc(YMODEM_CAN);
                    return -2;
                } else {
                    continue;
                }
            } else {
                continue;
            }
        case YMODEM_STAT_FINISHING:
            if(_recv_finishing(ymodem) < 0) {
                if(timeout++ > ymodem->timeout_s) {
                    ymodem->stat = YMODEM_STAT_STARTING;
                    ymodem->putc(YMODEM_CAN);
                    return -3;
                } else {
                    continue;
                }
            } else {
                ymodem->stat = YMODEM_STAT_FINISHED;
            }
            ymodem->putc(YMODEM_ACK);
            return 1;
        default:
            return -4;
        }
    }
    return 0;
}
